﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Windows file system implementation of <see cref="IFileSystem"/>.
   /// For compatibility with other file system, the constructor takes a root
   /// path which is combined with relative paths passed to the various methods.
   /// </summary>
   public class FileSystem : IFileSystem
   {
      readonly string root;
      readonly Task nothing = TaskEx.FromResult<object>(null);

      /// <summary>
      /// Creates instance.
      /// </summary>
      /// <param name="root">Path to use as root of this file system or either null or an empty string to use the full file system.</param>
      public FileSystem(string root = null)
      {
         this.root = root ?? "";
      }

      /// <summary>
      /// The root folder or "" when using the entire file system.
      /// </summary>
      public string Root { get { return root; } }

      /// <summary>
      /// Creates a folder.
      /// If the folder already exists, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public void CreateFolder(string path)
      {
         var fullPath = Path.Append(root, path);
         Directory.CreateDirectory(fullPath);
      }

      /// <summary>
      /// Creates a folder.
      /// If the folder already exists, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public Task CreateFolderAsync(string path)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         CreateFolder(path);
         return nothing;
      }

      /// <summary>
      /// Deletes a folder.
      /// If the folder does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public bool DeleteFolder(string path)
      {
         var fullPath = Path.Append(root, path);
         var existed = Directory.Exists(fullPath);
         if (existed)
            Directory.Delete(fullPath);
         return existed;
      }

      /// <summary>
      /// Deletes a folder.
      /// If the folder does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public Task<bool> DeleteFolderAsync(string path)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         var existed = DeleteFolder(path);
         return TaskEx.FromResult(existed);
      }

      /// <summary>
      /// Determines whether a folder exists.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public bool FolderExists(string path)
      {
         var fullPath = Path.Append(root, path);
         var folder = new DirectoryInfo(fullPath);
         return folder.Exists;
      }

      /// <summary>
      /// Determines whether a folder exists.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public Task<bool> FolderExistsAsync(string path)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         var existed = FolderExists(path);
         return TaskEx.FromResult(existed);
      }

      /// <summary>
      /// Enumerates the files in a folder.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <param name="searchPattern">The search pattern, e.g. "*.txt" or "Doc?.txt.</param>
      /// <param name="recurse">True to recurse into subfolders, false to only check the specified folder.</param>
      /// <returns>Paths to files which match the search pattern.</returns>
      public IEnumerable<string> EnumerateFiles(string path, string searchPattern, bool recurse)
      {
         var fullPath = Path.Append(root, path);
         var searchOption = recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
         var fileFullPaths = Directory.EnumerateFiles(fullPath, searchPattern, searchOption);

         if (root.Length == 0)
            return fileFullPaths;

         var relativePaths = fileFullPaths.Select(fileFullPath => fileFullPath.Substring(root.Length + 1));
         return relativePaths;
      }

      /// <summary>
      /// Enumerates the files in a folder.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <param name="searchPattern">The search pattern, e.g. "*.txt" or "Doc?.txt.</param>
      /// <param name="recurse">True to recurse into subfolders, false to only check the specified folder.</param>
      /// <returns>Paths to files which match the search pattern.</returns>
      public Task<IEnumerable<string>> EnumerateFilesAsync(string path, string searchPattern, bool recurse)
      {
         // Since enumeration could take some time, go ahead and do it on a separate thread.
         return TaskEx.Run(() => (IEnumerable<string>)EnumerateFiles(path, searchPattern, recurse).ToArray());
      }

      /// <summary>
      /// Moves and/or renames a file.
      /// An exception will be thrown if the destination file already exists.
      /// </summary>
      /// <param name="sourcePath">The path to the source file.</param>
      /// <param name="destinationPath">The path to the destination file.</param>
      public void MoveFile(string sourcePath, string destinationPath)
      {
         var fullSourcePath = Path.Append(root, sourcePath);
         var fullDestinationPath = Path.Append(root, destinationPath);
         try
         {
            File.Move(fullSourcePath, fullDestinationPath);
         }
         catch (IOException) // source doesn't exist or destination already exists
         {
            throw new InvalidOperationException("Cannot move file because source doesn't exist or destination already exists.");
         }
      }

      /// <summary>
      /// Moves and/or renames a file.
      /// An exception will be thrown if the destination file already exists.
      /// </summary>
      /// <param name="sourcePath">The path to the source file.</param>
      /// <param name="destinationPath">The path to the destination file.</param>
      public Task MoveFileAsync(string sourcePath, string destinationPath)
      {
         // Since enumeration could take some time, go ahead and do it on a separate thread.
         return TaskEx.Run(() => MoveFile(sourcePath, destinationPath));
      }

      /// <summary>
      /// Determines whether a file exists.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public bool FileExists(string path)
      {
         var fullPath = Path.Append(root, path);
         return File.Exists(fullPath);
      }

      /// <summary>
      /// Determines whether a file exists.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public Task<bool> FileExistsAsync(string path)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         var exists = FileExists(path);
         return TaskEx.FromResult(exists);
      }

      /// <summary>
      /// Deletes a file.
      /// If the file does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public bool DeleteFile(string path)
      {
         var fullPath = Path.Append(root, path);
         var existed = File.Exists(fullPath);
         File.Delete(fullPath);
         return existed;
      }

      /// <summary>
      /// Deletes a file.
      /// If the file does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public Task<bool> DeleteFileAsync(string path)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         var existed = DeleteFile(path);
         return TaskEx.FromResult(existed);
      }

      /// <summary>
      /// Writes text to a file.
      /// If the file does not exist then it's created; if it does exist then it's overwritten.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="text">The contents of the file.</param>
      public void WriteAllText(string path, string text)
      {
         var fullPath = Path.Append(root, path);
         File.WriteAllText(fullPath, text);
      }

      /// <summary>
      /// Writes text to a file.
      /// If the file does not exist then it's created; if it does exist then it's overwritten.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="text">The contents of the file.</param>
      public Task WriteAllTextAsync(string path, string text)
      {
         // Mix of quick synchronous open followed by may-take-a-while asynchronous writing.
         using (var stream = OpenForWrite(path, OpenMode.Create))
         using (var writer = new StreamWriter(stream))
            return writer.WriteAsync(text);
      }

      /// <summary>
      /// Reads the contents of an existing file as text.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>The contents of the file.</returns>
      public string ReadAllText(string path)
      {
         var fullPath = Path.Append(root, path);
         return File.ReadAllText(fullPath);
      }

      /// <summary>
      /// Reads the contents of an existing file as text.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>The contents of the file.</returns>
      public Task<string> ReadAllTextAsync(string path)
      {
         // Mix of quick synchronous open followed by may-take-a-while asynchronous reading.
         using (var stream = OpenForRead(path))
         using (var reader = new StreamReader(stream))
            return reader.ReadToEndAsync();
      }

      /// <summary>
      /// Opens an existing file for reading.
      /// The sharing mode is read-only.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from but not written to; must be disposed to close file.</returns>
      public Stream OpenForRead(string path, OpenMode mode = OpenMode.Open, OpenShare share = OpenShare.Read)
      {
         var fullPath = Path.Append(root, path);
         return File.Open(fullPath, ToMode(mode), FileAccess.Read, ToShare(share));
      }

      /// <summary>
      /// Opens an existing file for reading.
      /// The sharing mode is read-only.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from but not written to; must be disposed to close file.</returns>
      public Task<Stream> OpenForReadAsync(string path, OpenMode mode = OpenMode.Open, OpenShare share = OpenShare.Read)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         var stream = OpenForRead(path, mode, share);
         return TaskEx.FromResult(stream);
      }

      /// <summary>
      /// Opens a file for writing; if the file does not exist, it is created.
      /// The sharing mode is none.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from and written to; must be disposed to close file.</returns>
      public Stream OpenForWrite(string path, OpenMode mode = OpenMode.OpenOrCreate, OpenShare share = OpenShare.None)
      {
         var fullPath = Path.Append(root, path);
         // Read/write access matches what win8 provides
         return File.Open(fullPath, ToMode(mode), FileAccess.ReadWrite, ToShare(share));
      }

      /// <summary>
      /// Opens a file for writing; if the file does not exist, it is created.
      /// The sharing mode is none.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from and written to; must be disposed to close file.</returns>
      public Task<Stream> OpenForWriteAsync(string path, OpenMode mode = OpenMode.OpenOrCreate, OpenShare share = OpenShare.None)
      {
         // No async version of this in .NET 45 and creating a thread to do this is likely less efficient 
         // than just doing it synchronously and returning the result as an already-completed task.
         var stream = OpenForWrite(path, mode, share);
         return TaskEx.FromResult(stream);
      }

      static FileMode ToMode(OpenMode mode)
      {
         switch (mode)
         {
            case OpenMode.CreateNew:
               return FileMode.CreateNew;
            case OpenMode.Create:
               return FileMode.Create;
            case OpenMode.Open:
               return FileMode.Open;
            case OpenMode.OpenOrCreate:
               return FileMode.OpenOrCreate;
            //case OpenMode.Truncate:
            //   return FileMode.Truncate;
            //case OpenMode.Append:
            //   return FileMode.Append;
            default:
               throw new ArgumentOutOfRangeException("mode");
         }
      }

      //static FileAccess ToAccess(OpenAccess access)
      //{
      //   switch (access)
      //   {
      //      case OpenAccess.Read:
      //         return FileAccess.Read;
      //      case OpenAccess.Write:
      //         return FileAccess.Write;
      //      case OpenAccess.ReadWrite:
      //         return FileAccess.ReadWrite;
      //      default:
      //         throw new ArgumentOutOfRangeException("access");
      //   }
      //}

      static FileShare ToShare(OpenShare share)
      {
         switch (share)
         {
            case OpenShare.None:
               return FileShare.None;
            case OpenShare.Read:
               return FileShare.Read;
            case OpenShare.Write:
               return FileShare.Write;
            case OpenShare.ReadWrite:
               return FileShare.ReadWrite;
            case OpenShare.Delete:
               return FileShare.Delete;
            default:
               throw new ArgumentOutOfRangeException("share");
         }
      }
   }
}
